|
- <template>
- <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8 lg:py-10">
- <nav aria-label="面包屑" class="flex items-center gap-1.5 text-xs text-[var(--c-8a97a8)]">
- <NuxtLink to="/" class="hover:text-[var(--c-0b8cff)]">首页</NuxtLink>
- <span>/</span>
- <NuxtLink to="/pic/" class="hover:text-[var(--c-0b8cff)]">AI 图片</NuxtLink>
- <span>/</span>
- <span class="line-clamp-1 text-[var(--c-4b5b70)]">{{ pageTitle }}</span>
- </nav>
-
- <div class="mt-5 lg:grid lg:grid-cols-12 lg:gap-8">
- <!-- 主图 -->
- <div class="lg:col-span-8">
- <div class="overflow-hidden rounded-2xl bg-[var(--c-ffffff)] p-4 shadow-[0_8px_28px_var(--sh-34-68-112-50)] sm:p-6">
- <div class="overflow-hidden rounded-xl bg-[var(--c-eef3f8)]">
- <img
- v-if="currentImg"
- :src="currentImg"
- :alt="pageTitle"
- class="mx-auto max-h-[70vh] w-auto object-contain"
- >
- </div>
-
- <!-- 多图切换 -->
- <div v-if="picList.length > 1" class="mt-4 flex flex-wrap gap-2">
- <button
- v-for="(img, idx) in picList"
- :key="idx"
- type="button"
- class="size-16 overflow-hidden rounded-lg border-2 transition-colors"
- :class="idx === imgIndex ? 'border-[var(--c-0b8cff)]' : 'border-transparent hover:border-[var(--c-e5edf7)]'"
- @click="imgIndex = idx"
- >
- <img :src="img" class="size-full object-cover" :alt="`图 ${idx + 1}`" loading="lazy">
- </button>
- </div>
-
- <!-- 信息 -->
- <div class="mt-6 border-t border-[var(--c-eef2f7)] pt-5">
- <h1 class="text-lg font-extrabold leading-snug text-[var(--c-172033)] sm:text-xl">{{ pageTitle }}</h1>
-
- <div class="mt-3 flex flex-wrap items-center gap-3 text-xs text-[var(--c-a0aabb)]">
- <span class="rounded-full bg-[var(--c-f3f0ff)] px-2 py-0.5 font-bold text-[var(--c-7a5af8)]">{{ aiTypeName }}</span>
- <span v-if="detail.author_name" class="inline-flex items-center gap-1">
- <UIcon name="i-lucide-user-round" class="size-3.5" />
- {{ detail.author_name }}
- </span>
- <span class="inline-flex items-center gap-1">
- <UIcon name="i-lucide-calendar" class="size-3.5" />
- {{ formatTime(detail.add_time) }}
- </span>
- <span v-if="detail.view_num" class="inline-flex items-center gap-1">
- <UIcon name="i-lucide-eye" class="size-3.5" />
- {{ detail.view_num }}
- </span>
- <span v-if="detail.like_num" class="inline-flex items-center gap-1">
- <UIcon name="i-lucide-heart" class="size-3.5" />
- {{ detail.like_num }}
- </span>
- <span v-if="detail.resolution" class="inline-flex items-center gap-1">
- <UIcon name="i-lucide-ratio" class="size-3.5" />
- {{ detail.resolution }}
- </span>
- </div>
-
- <!-- 转化 -->
- <div class="mt-6 flex flex-wrap gap-3">
- <NuxtLink
- to="/draw/anime/"
- class="inline-flex h-11 items-center gap-1.5 rounded-full bg-gradient-to-r from-[var(--c-0b8cff)] to-[var(--c-3aa9ff)] px-6 text-sm font-bold text-white shadow-[0_10px_22px_var(--sh-11-140-255-240)] transition-opacity hover:opacity-90"
- >
- <UIcon name="i-lucide-zap" class="size-4" />
- 我也要生成
- </NuxtLink>
- <a
- v-if="currentImg"
- :href="currentImg"
- target="_blank"
- rel="noopener"
- class="inline-flex h-11 items-center gap-1.5 rounded-full bg-[var(--c-eef7ff)] px-6 text-sm font-bold text-[var(--c-0b8cff)] transition-colors hover:bg-[var(--c-e0f0ff)]"
- >
- <UIcon name="i-lucide-external-link" class="size-4" />
- 查看原图
- </a>
- </div>
-
- <!-- 上一件 / 下一件(同时利于站内链接与收录) -->
- <div
- v-if="detail.prev_chat_open_id || detail.next_chat_open_id"
- class="mt-6 grid gap-3 border-t border-[var(--c-eef2f7)] pt-5 sm:grid-cols-2"
- >
- <NuxtLink
- v-if="detail.prev_chat_open_id"
- :to="`/pic/${detail.prev_chat_open_id}_0.html`"
- rel="prev"
- class="group flex items-center gap-2 rounded-xl bg-[var(--c-f7f9fd)] p-4 transition-colors hover:bg-[var(--c-eef7ff)]"
- >
- <UIcon name="i-lucide-chevron-left" class="size-4 shrink-0 text-[var(--c-a0aabb)]" />
- <div class="min-w-0">
- <div class="text-xs text-[var(--c-a0aabb)]">上一件作品</div>
- <div class="mt-0.5 text-sm font-bold text-[var(--c-4b5b70)] transition-colors group-hover:text-[var(--c-0b8cff)]">查看</div>
- </div>
- </NuxtLink>
- <NuxtLink
- v-if="detail.next_chat_open_id"
- :to="`/pic/${detail.next_chat_open_id}_0.html`"
- rel="next"
- class="group flex items-center justify-end gap-2 rounded-xl bg-[var(--c-f7f9fd)] p-4 text-right transition-colors hover:bg-[var(--c-eef7ff)] sm:col-start-2"
- >
- <div class="min-w-0">
- <div class="text-xs text-[var(--c-a0aabb)]">下一件作品</div>
- <div class="mt-0.5 text-sm font-bold text-[var(--c-4b5b70)] transition-colors group-hover:text-[var(--c-0b8cff)]">查看</div>
- </div>
- <UIcon name="i-lucide-chevron-right" class="size-4 shrink-0 text-[var(--c-a0aabb)]" />
- </NuxtLink>
- </div>
- </div>
- </div>
- </div>
-
- <!-- 相关作品 -->
- <div class="mt-8 lg:col-span-4 lg:mt-0">
- <div class="rounded-2xl bg-[var(--c-ffffff)] p-5 shadow-[0_8px_28px_var(--sh-34-68-112-50)]">
- <h2 class="text-base font-extrabold text-[var(--c-172033)]">更多 AI 作品</h2>
- <div v-if="otherList.length" class="mt-4 grid grid-cols-3 gap-2">
- <NuxtLink
- v-for="item in otherList"
- :key="`${item.chat_open_id}_${item.img_index}`"
- :to="`/pic/${item.chat_open_id}_${item.img_index || 0}.html`"
- class="group aspect-square overflow-hidden rounded-lg bg-[var(--c-eef3f8)]"
- >
- <img
- :src="item.img"
- :alt="item.prompts || 'AI 作品'"
- class="size-full object-cover transition-transform duration-300 group-hover:scale-110"
- loading="lazy"
- >
- </NuxtLink>
- </div>
- <div v-else class="py-8 text-center text-sm text-[var(--c-a0aabb)]">暂无更多作品</div>
- <NuxtLink
- to="/pic/"
- class="mt-4 flex items-center justify-center gap-1 text-sm font-bold text-[var(--c-0b8cff)] hover:underline"
- >
- 查看全部图片
- <UIcon name="i-lucide-arrow-right" class="size-4" />
- </NuxtLink>
- </div>
- </div>
- </div>
- </div>
- </template>
-
- <script setup>
- import dayjs from 'dayjs'
- import { legacyPageTitle } from '~/composables/useLegacySeo'
-
- // ai_type:1 gpt 2 MJ绘画 3/7/8 AI画头像 4 变年龄 5 变性别 6 AI绘画(沿用原站映射)
- const AI_TYPE_MAP = {
- 1: 'AI 问答', 2: 'MJ 绘画', 3: 'AI 画头像', 4: 'AI 变年龄',
- 5: 'AI 变性别', 6: 'AI 绘画', 7: 'AI 画头像', 8: 'AI 画头像',
- }
- const SEO_AI_TYPE_MAP = {
- 1: 'gpt', 2: 'MJ绘画', 3: 'AI画头像', 4: 'AI变年龄',
- 5: 'AI变性别', 6: 'AI绘画', 7: 'AI画头像', 8: 'AI画头像',
- }
-
- const route = useRoute()
- const { post } = useApi()
-
- // 原站 URL 形式:/pic/{chat_open_id}_{img_index}.html
- const raw = String(route.params.id || '').replace(/\.html$/i, '')
- const chatOpenId = raw.split('_')[0]
- const initialIndex = Number(raw.split('_')[1] || 0)
-
- const { data, error } = await useAsyncData(`pic-${raw}`, async () => {
- const detailRes = await post('/art/getdetail', { chat_open_id: chatOpenId, prev_next: 1 })
- const d = detailRes.data || {}
- const otherRes = await post('/art/getlist', { page_no: 1, page_size: 12, curr_id: d.id })
- .catch(() => ({ list: [] }))
- return { detail: d, otherList: (otherRes.list || []).filter(i => i.img).slice(0, 9) }
- })
-
- if (error.value || !data.value?.detail) {
- throw createError({ statusCode: 404, statusMessage: '作品不存在', fatal: true })
- }
-
- const detail = computed(() => data.value.detail || {})
- const otherList = computed(() => data.value.otherList || [])
- const picList = computed(() => (detail.value.content ? String(detail.value.content).split(',') : []))
- const imgIndex = ref(initialIndex)
- const currentImg = computed(() => picList.value[imgIndex.value] || picList.value[0] || '')
- const aiTypeName = computed(() => AI_TYPE_MAP[Number(detail.value.ai_type)] || 'AI 作品')
- const seoAiTypeName = computed(() => SEO_AI_TYPE_MAP[Number(detail.value.ai_type)] || 'AI作品')
-
- function formatTime(v) {
- return v ? dayjs(Number(v) * 1000).format('YYYY-MM-DD HH:mm') : ''
- }
-
- // 页面内展示标题保持现有样式。
- const pageTitle = computed(() => {
- const p = String(detail.value.prompts || '').trim()
- if (p) return p
- const day = detail.value.add_time ? dayjs(Number(detail.value.add_time) * 1000).format('YYYYMMDD') : ''
- return `AI会员生成的${aiTypeName.value}(${day})`
- })
-
- // TDK 严格沿用旧 PC 站 pic/detail controller 的生成规则。
- const seoPageTitle = computed(() => {
- const prompts = String(detail.value.prompts || '').trim()
- if (prompts) return prompts
- const day = detail.value.add_time
- ? dayjs(Number(detail.value.add_time) * 1000).format('YYYYMMDD')
- : ''
- return `AI会员(${detail.value.author_open_id})生成的${seoAiTypeName.value}(${day}00${picList.value.length})`
- })
- const seoDescription = computed(() => {
- const day = detail.value.add_time
- ? dayjs(Number(detail.value.add_time) * 1000).format('YYYYMMDD')
- : ''
- return `由会员(${detail.value.author_open_id})(${day})生成的${seoAiTypeName.value}`
- })
-
- useSeoMeta({
- title: () => legacyPageTitle(seoPageTitle.value),
- description: seoDescription,
- keywords: 'AI画头像',
- ogTitle: seoPageTitle,
- ogType: 'article',
- ogImage: currentImg,
- })
- useHead({
- link: [{ rel: 'canonical', href: `https://tool.aionline.cc/pic/${raw}.html` }],
- script: [{
- type: 'application/ld+json',
- innerHTML: computed(() => JSON.stringify({
- '@context': 'https://schema.org',
- '@type': 'ImageObject',
- name: pageTitle.value,
- contentUrl: currentImg.value,
- description: detail.value.prompts || aiTypeName.value,
- creator: { '@type': 'Organization', name: 'AI在线' },
- })),
- }],
- })
- </script>
|